package lumis.util.converter;

import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * This class offers utility conversion methods.
 * 
 * @since 4.0.4
 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
 */
public class ConvertUtil
{
	private static Map<Class, Class<? extends IConverter>> converterClasses = new HashMap<Class, Class<? extends IConverter>>();
	static
	{
		register(Integer.class, NumberConverter.class);
		register(Byte.class, NumberConverter.class);
		register(Long.class, NumberConverter.class);
		register(Float.class, NumberConverter.class);
		register(Double.class, NumberConverter.class);
		register(Short.class, NumberConverter.class);
		register(java.sql.Date.class, DateConverter.class);
		register(java.sql.Time.class, TimeConverter.class);
		register(java.sql.Timestamp.class, DateTimeConverter.class);
		register(java.util.Date.class, DateTimeConverter.class);
		register(Boolean.class, BooleanConverter.class);
		register(Collection.class, CollectionConverter.class);
		register(Object[].class, CollectionConverter.class);
	}	

	public static void register(Class type, Class<? extends IConverter> converterClass)
	{
		converterClasses.put(type, converterClass);
	}

	public static void deregister(Class type)
	{
		converterClasses.remove(type);
	}
	
	/**
	 * A converter that delegates calls to it to a registered converter.
	 * @see DelegateToRegisteredConverter
	 * @since 4.0.4
	 */
	public static final IConverter DELEGATE_TO_REGISTERED_CONVERTER = new DelegateToRegisteredConverter();
	
	/**
	 * Try to find a converter for the specified type.
	 * If the class has not a converter registered, try the superclasses and
	 * the interfaces that the class implement.
	 * @param type	the type that needs to be converted.
	 * @return		the converter class type or null if a converter is not found.
	 * @since 6.0.0
	 */
	private static Class<? extends IConverter> findConverter(Class type)
	{
		Class<? extends IConverter> converterClass = converterClasses.get(type);
		
		// array		
		if ((converterClass == null) && (type.isArray()))
		{
			converterClass = converterClasses.get(Object[].class);
		}
		
		// try the superclasses if the class has not a converter registered
		if (converterClass == null)
		{
			Class superclass = type.getSuperclass();
			while ((superclass != null) && (superclass != Object.class))
			{
				converterClass = converterClasses.get(superclass);
				if (converterClass != null)
				{
					return converterClass;
				}
				superclass = superclass.getSuperclass();
			}
		}
		
		if (converterClass == null)
		{
			// search for a converter in the interfaces of the type
			converterClass = findConverterForInterface(type);
		}
		return converterClass;
	}
	
	/**
	 * Try to find a converter for the type searching in the interfaces that
	 * the type implements.
	 * @param type	the type that needs to be converted.
	 * @return	the converter class type or null if a converter is not found.
	 * @since 6.0.0
	 */
	private static Class<? extends IConverter> findConverterForInterface(Class type)
	{
		Class<? extends IConverter> converterClass;
		
		for (Class interfaceClass : type.getInterfaces())
		{
			converterClass = converterClasses.get(interfaceClass);
			if (converterClass != null)
			{
				return converterClass;
			}
		}
		// didn't find a converter for the interfaces
		
		// search for a converter in super types of the interfaces
		for (Class interfaceClass : type.getInterfaces())
		{
			converterClass = findConverterForInterface(interfaceClass);
			if (converterClass != null)
			{
				return converterClass;
			}
		}
		
		return null;
	}

	/**
	 * Create the appropriated converter for the given type.
	 * @param type	the type that needs to be converted.
	 * @return		an instance of the appropriated converter for the given type.
	 * @throws UnsupportedConversionTypeException	if the conversion is not supported.
	 * @since 6.0.0
	 */
	public static IConverter createConverter(Class type) throws UnsupportedConversionTypeException
	{
		Class<? extends IConverter> converterClass = findConverter(type); 
		
		if (converterClass != null)
		{
			try
			{
				return converterClass.newInstance();
			}
			catch (InstantiationException e)
			{
				throw new UnsupportedConversionTypeException(e);
			}
			catch (IllegalAccessException e)
			{
				throw new UnsupportedConversionTypeException(e);
			}
		}
		else
		{
			throw new UnsupportedConversionTypeException(type.getName());	
		}		
	}
	
	@SuppressWarnings("unchecked")
	public static <T> T convert(Class<T> type, Object value, Locale locale, String pattern)
	{
		try
		{
			if (value == null)
				return null;
			if (type.isAssignableFrom(value.getClass()))
				return (T)value;

			IConverter converter = createConverter(type);
			
			// disable fallback to registered converters to prevent an infinite loop
			if (converter instanceof AbstractConverter)
				((AbstractConverter) converter).setFallbackToRegisteredConverters(false);
			
			// do the conversion
			return converter.convert(type, value, locale, pattern);			
		}
		catch (UnsupportedConversionTypeException e)
		{
			// try to use value's class converter
			IConverter converter;
			try
			{
				converter = createConverter(value.getClass());
			}
			catch (UnsupportedConversionTypeException e2)
			{
				if (type.isAssignableFrom(String.class))
				{
					return (T) value.toString();	
				}
				else
				{
					throw e;
				}	
			}
			
			// disable fallback to registered converters to prevent an infinite loop
			if (converter instanceof AbstractConverter)
				((AbstractConverter)converter).setFallbackToRegisteredConverters(false);
			
			// do the conversion
			return converter.convert(type, value, locale, pattern);
		}
	}
	
	public static <T,R extends Collection<T>> Collection<T> convert(Class<T> valueType, Iterable input, R output, Locale locale, String pattern)
	{
		for (Object value : input)
			output.add(convert(valueType, value, locale, pattern));
		return output;
	}
}
